﻿/*
 * $Id: JSONReader.cs 439 2007-11-26 13:26:10Z spocke $
 *
 * Copyright © 2007, Moxiecode Systems AB, All rights reserved. 
 */

using System;
using System.IO;
using System.Text;
using System.Collections;

namespace mojoPortal.Web.Editor
{
    /// <summary>
    /// 
    /// </summary>
    public enum JSONToken
    {
        /// <summary> </summary>
        Boolean,

        /// <summary> </summary>
        Integer,

        /// <summary> </summary>
        String,

        /// <summary> </summary>
        Null,

        /// <summary> </summary>
        Float,

        /// <summary> </summary>
        StartArray,

        /// <summary> </summary>
        EndArray,

        /// <summary> </summary>
        PropertyName,

        /// <summary> </summary>
        StartObject,

        /// <summary> </summary>
        EndObject
    }

    /// <summary>
    ///  Description of JSONReader.
    /// </summary>
    public class JSONReader
    {
        private TextReader reader;
        private JSONToken token;
        private object val;
        private JSONLocation location;
        private Stack lastLocations;
        private bool needProp;

        /// <summary>
        /// 
        /// </summary>
        /// <param name="reader"></param>
        public JSONReader(TextReader reader)
        {
            this.reader = reader;
            this.val = null;
            this.token = JSONToken.Null;
            this.location = JSONLocation.Normal;
            this.lastLocations = new Stack();
            this.needProp = false;
        }

        /// <summary>
        /// 
        /// </summary>
        public JSONLocation Location
        {
            get { return location; }
        }

        /// <summary>
        /// 
        /// </summary>
        public JSONToken TokenType
        {
            get
            {
                return this.token;
            }
        }

        /// <summary>
        /// 
        /// </summary>
        public object Value
        {
            get
            {
                return this.val;
            }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <returns></returns>
        public bool Read()
        {
            int chr = this.reader.Read();

            if (chr != -1)
            {
                switch ((char)chr)
                {
                    case '[':
                        this.lastLocations.Push(this.location);
                        this.location = JSONLocation.InArray;
                        this.token = JSONToken.StartArray;
                        this.val = null;
                        this.ReadAway();
                        return true;

                    case ']':
                        this.location = (JSONLocation)this.lastLocations.Pop();
                        this.token = JSONToken.EndArray;
                        this.val = null;
                        this.ReadAway();

                        if (this.location == JSONLocation.InObject)
                            this.needProp = true;

                        return true;

                    case '{':
                        this.lastLocations.Push(this.location);
                        this.location = JSONLocation.InObject;
                        this.needProp = true;
                        this.token = JSONToken.StartObject;
                        this.val = null;
                        this.ReadAway();
                        return true;

                    case '}':
                        this.location = (JSONLocation)this.lastLocations.Pop();
                        this.token = JSONToken.EndObject;
                        this.val = null;
                        this.ReadAway();

                        if (this.location == JSONLocation.InObject)
                            this.needProp = true;

                        return true;

                    // String
                    case '"':
                    case '\'':
                        return this.ReadString((char)chr);

                    // Null
                    case 'n':
                        return this.ReadNull();

                    // Bool
                    case 't':
                    case 'f':
                        return this.ReadBool((char)chr);

                    default:
                        // Is number
                        if (Char.IsNumber((char)chr) || (char)chr == '-' || (char)chr == '.')
                            return this.ReadNumber((char)chr);

                        return true;
                }
            }

            return false;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <returns></returns>
        public override string ToString()
        {
            switch (this.token)
            {
                case JSONToken.Boolean:
                    return "[Boolean] = " + ((bool)this.Value ? "true" : "false");

                case JSONToken.EndArray:
                    return "[EndArray]";

                case JSONToken.EndObject:
                    return "[EndObject]";

                case JSONToken.Float:
                    return "[Float] = " + Convert.ToDouble(this.Value);

                case JSONToken.Integer:
                    return "[Integer] = " + ((int)this.Value);

                case JSONToken.Null:
                    return "[Null]";

                case JSONToken.StartArray:
                    return "[StartArray]";

                case JSONToken.StartObject:
                    return "[StartObject]";

                case JSONToken.String:
                    return "[String]" + (string)this.Value;

                case JSONToken.PropertyName:
                    return "[PropertyName]" + (string)this.Value;
            }

            return base.ToString();
        }

        #region private methods

        private bool ReadString(char quote)
        {
            StringBuilder buff = new StringBuilder();
            this.token = JSONToken.String;
            bool endString = false;
            int chr;

            while ((chr = this.reader.Peek()) != -1)
            {
                switch (chr)
                {
                    case '\\':
                        // Read away slash
                        chr = this.reader.Read();

                        // Read escape code
                        chr = this.reader.Read();
                        switch (chr)
                        {
                            case 't':
                                buff.Append('\t');
                                break;

                            case 'b':
                                buff.Append('\b');
                                break;

                            case 'f':
                                buff.Append('\f');
                                break;

                            case 'r':
                                buff.Append('\r');
                                break;

                            case 'n':
                                buff.Append('\n');
                                break;

                            case 'u':
                                buff.Append((char)Convert.ToInt32(ReadLen(4), 16));
                                break;

                            default:
                                buff.Append((char)chr);
                                break;
                        }

                        break;

                    case '\'':
                    case '"':
                        if (chr == quote)
                            endString = true;

                        chr = this.reader.Read();
                        if (chr != -1 && chr != quote)
                            buff.Append((char)chr);

                        break;

                    default:
                        buff.Append((char)this.reader.Read());
                        break;
                }

                // String terminated
                if (endString)
                    break;
            }

            this.ReadAway();

            this.val = buff.ToString();

            // Needed a property
            if (this.needProp)
            {
                this.token = JSONToken.PropertyName;
                this.needProp = false;
                return true;
            }

            if (this.location == JSONLocation.InObject && !this.needProp)
                this.needProp = true;

            return true;
        }

        private bool ReadNull()
        {
            this.token = JSONToken.Null;
            this.val = null;

            this.ReadAway(3); // ull
            this.ReadAway();

            if (this.location == JSONLocation.InObject && !this.needProp)
                this.needProp = true;

            return true;
        }

        private bool ReadNumber(char start)
        {
            StringBuilder buff = new StringBuilder();
            int chr;
            bool isFloat = false;

            this.token = JSONToken.Integer;
            buff.Append(start);

            while ((chr = this.reader.Peek()) != -1)
            {
                if (Char.IsNumber((char)chr) || (char)chr == '-' || (char)chr == '.')
                {
                    if (((char)chr) == '.')
                        isFloat = true;

                    buff.Append((char)this.reader.Read());
                }
                else
                    break;
            }

            this.ReadAway();

            if (isFloat)
            {
                this.token = JSONToken.Float;
                this.val = Convert.ToDouble(buff.ToString().Replace('.', ','));
            }
            else
                this.val = Convert.ToInt32(buff.ToString());

            if (this.location == JSONLocation.InObject && !this.needProp)
                this.needProp = true;

            return true;
        }

        private bool ReadBool(char chr)
        {
            this.token = JSONToken.Boolean;
            this.val = chr == 't';

            if (chr == 't')
                this.ReadAway(3); // rue
            else
                this.ReadAway(4); // alse

            this.ReadAway();

            if (this.location == JSONLocation.InObject && !this.needProp)
                this.needProp = true;

            return true;
        }

        private void ReadAway()
        {
            int chr;

            while ((chr = this.reader.Peek()) != -1)
            {
                if (chr != ':' && chr != ',' && !Char.IsWhiteSpace((char)chr))
                    break;

                this.reader.Read();
            }
        }

        private string ReadLen(int num)
        {
            StringBuilder buff = new StringBuilder();
            int chr;

            for (int i = 0; i < num && (chr = this.reader.Read()) != -1; i++)
                buff.Append((char)chr);

            return buff.ToString();
        }

        private void ReadAway(int num)
        {
            for (int i = 0; i < num && this.reader.Read() != -1; i++) ;
        }

        #endregion
    }
}
